/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.impl.wsdl.support.http;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.SoapUISystemProperties;
import com.eviware.soapui.impl.wsdl.submit.transports.http.ExtendedHttpMethod;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.metrics.SoapUIMetrics;
import com.eviware.soapui.impl.wsdl.support.CompressionSupport;
import com.eviware.soapui.model.propertyexpansion.PropertyExpander;
import com.eviware.soapui.model.settings.Settings;
import com.eviware.soapui.model.settings.SettingsListener;
import com.eviware.soapui.settings.HttpSettings;
import com.eviware.soapui.settings.SSLSettings;
import org.apache.commons.ssl.KeyMaterial;
import org.apache.http.Header;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.log4j.Logger;

import java.io.File;
import java.io.IOException;
import java.net.ProxySelector;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

/**
 * HttpClient related tools
 *
 * @author Ole.Matzura
 */

public class HttpClientSupport {
    private final static Helper helper = new Helper();

    static {
        if (PropertyExpander.getDefaultExpander() == null) {
            SoapUI.log.warn("Default property expander was null - will set global proxy later");
        } else {
            ProxyUtils.setGlobalProxy(SoapUI.getSettings());
        }
    }

    /**
     * Internal helper to ensure synchronized access..
     */

    public static class SoapUIHttpClient extends DefaultHttpClient {
        public SoapUIHttpClient(final ClientConnectionManager conman) {
            super(conman, null);
        }

        @Override
        protected HttpRequestExecutor createRequestExecutor() {
            return new SoapUIHttpRequestExecutor();
        }

    }

    public static class SoapUIHttpRequestExecutor extends HttpRequestExecutor {

        @Override
        public void preProcess(final HttpRequest request, final HttpProcessor processor, final HttpContext context)
                throws HttpException, IOException {
            HttpRequest original = request;

            if (original instanceof RequestWrapper) {
                RequestWrapper w = (RequestWrapper) request;
                original = w.getOriginal();
            }

            if (original instanceof ExtendedHttpMethod) {
                SoapUIMetrics metrics = ((ExtendedHttpMethod) original).getMetrics();
                metrics.getConnectTimer().stop();
                metrics.getTimeToFirstByteTimer().start();
            }
            super.preProcess(request, processor, context);
        }

        @Override
        protected HttpResponse doSendRequest(HttpRequest request, HttpClientConnection conn, HttpContext context)
                throws IOException, HttpException {
            HttpResponse response = super.doSendRequest(request, conn, context);
            return response;
        }

        @Override
        protected HttpResponse doReceiveResponse(final HttpRequest request, final HttpClientConnection conn,
                                                 final HttpContext context) throws HttpException, IOException {
            if (request == null) {
                throw new IllegalArgumentException("HTTP request may not be null");
            }
            if (conn == null) {
                throw new IllegalArgumentException("HTTP connection may not be null");
            }
            if (context == null) {
                throw new IllegalArgumentException("HTTP context may not be null");
            }

            HttpResponse response = null;
            int statuscode = 0;

            HttpRequest original = request;

            if (original instanceof RequestWrapper) {
                RequestWrapper w = (RequestWrapper) request;
                original = w.getOriginal();
            }

            while (response == null || statuscode < HttpStatus.SC_OK) {
                response = conn.receiveResponseHeader();

                SoapUIMetrics metrics = null;
                if (original instanceof ExtendedHttpMethod) {
                    metrics = ((ExtendedHttpMethod) original).getMetrics();
                    metrics.getTimeToFirstByteTimer().stop();
                    metrics.getReadTimer().start();
                }

                if (canResponseHaveBody(request, response)) {
                    conn.receiveResponseEntity(response);
                    //	if( metrics != null ) {
                    //	metrics.getReadTimer().stop();
                    // }
                }

                statuscode = response.getStatusLine().getStatusCode();

                if (conn.getMetrics() instanceof SoapUIMetrics) {
                    SoapUIMetrics connectionMetrics = (SoapUIMetrics) conn.getMetrics();

                    if (metrics != null && connectionMetrics != null && !connectionMetrics.isDone()) {
                        metrics.getDNSTimer().set(connectionMetrics.getDNSTimer().getStart(),
                                connectionMetrics.getDNSTimer().getStop());
                        // reset connection-level metrics
                        connectionMetrics.reset();
                    }
                }

            } // while intermediate response

            if (original instanceof ExtendedHttpMethod) {
                ExtendedHttpMethod extendedHttpMethod = (ExtendedHttpMethod) original;
                extendedHttpMethod.afterReadResponse(((SoapUIMultiThreadedHttpConnectionManager.SoapUIBasicPooledConnAdapter) conn).getSSLSession());
            }

            return response;
        }
    }

    private static class Helper {
        private final SoapUIHttpClient httpClient;
        private final static Logger log = Logger.getLogger(HttpClientSupport.Helper.class);
        private final SoapUIMultiThreadedHttpConnectionManager connectionManager;
        private final SchemeRegistry registry;

        public Helper() {
            Settings settings = SoapUI.getSettings();
            registry = new SchemeRegistry();

            registry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));

            try {
                SoapUISSLSocketFactory socketFactory = initSocketFactory();
                registry.register(new Scheme("https", 443, socketFactory));
            } catch (Throwable e) {
                SoapUI.logError(e);
            }

            connectionManager = new SoapUIMultiThreadedHttpConnectionManager(registry);
            connectionManager.setMaxTotal((int) settings.getLong(HttpSettings.MAX_TOTAL_CONNECTIONS, 2000));
            connectionManager
                    .setDefaultMaxPerRoute((int) settings.getLong(HttpSettings.MAX_CONNECTIONS_PER_HOST, 500));

            httpClient = new SoapUIHttpClient(connectionManager);

            // this interceptor needs to be last one added and executed.
            httpClient.addRequestInterceptor(new HeaderRequestInterceptor(), httpClient.getRequestInterceptorCount());

            settings.addSettingsListener(new SSLSettingsListener());
        }

        public SoapUIHttpClient getHttpClient() {
            return httpClient;
        }

        private SchemeRegistry getRegistry() {
            return registry;
        }

        public HttpResponse execute(ExtendedHttpMethod method, HttpContext httpContext) throws ClientProtocolException,
                IOException {
            method.afterWriteRequest();
            if (method.getMetrics() != null) {
                method.getMetrics().getConnectTimer().start();
            }
            HttpResponse httpResponse = httpClient.execute(method, httpContext);
            method.setHttpResponse(httpResponse);

            return httpResponse;
        }

        public HttpResponse execute(ExtendedHttpMethod method) throws ClientProtocolException, IOException {
            method.afterWriteRequest();
            if (method.getMetrics() != null) {
                method.getMetrics().getConnectTimer().start();
            }
            HttpResponse httpResponse = httpClient.execute(method);
            method.setHttpResponse(httpResponse);
            return httpResponse;
        }

        public final class SSLSettingsListener implements SettingsListener {
            @Override
            public void settingChanged(String name, String newValue, String oldValue) {

                if (name.equals(SSLSettings.KEYSTORE) || name.equals(SSLSettings.KEYSTORE_PASSWORD)) {
                    try {
                        log.info("Updating keyStore..");
                        registry.register(new Scheme("https", 443, initSocketFactory()));
                    } catch (Throwable e) {
                        SoapUI.logError(e);
                    }
                } else if (name.equals(HttpSettings.MAX_CONNECTIONS_PER_HOST)) {
                    log.info("Updating max connections per host to " + newValue);
                    connectionManager.setDefaultMaxPerRoute(Integer.parseInt(newValue));
                } else if (name.equals(HttpSettings.MAX_TOTAL_CONNECTIONS)) {
                    log.info("Updating max total connections host to " + newValue);
                    connectionManager.setMaxTotal(Integer.parseInt(newValue));
                }
            }

            @Override
            public void settingsReloaded() {
                try {
                    log.info("Updating keyStore..");
                    registry.register(new Scheme("https", 443, initSocketFactory()));
                } catch (Throwable e) {
                    SoapUI.logError(e);
                }
            }
        }

        public SoapUISSLSocketFactory initSocketFactory() throws KeyStoreException, NoSuchAlgorithmException,
                CertificateException, IOException, UnrecoverableKeyException, KeyManagementException {
            KeyStore keyStore = null;
            Settings settings = SoapUI.getSettings();

            String keyStoreUrl = System.getProperty(SoapUISystemProperties.SOAPUI_SSL_KEYSTORE_LOCATION,
                    settings.getString(SSLSettings.KEYSTORE, null));

            keyStoreUrl = keyStoreUrl != null ? keyStoreUrl.trim() : "";

            String pass = System.getProperty(SoapUISystemProperties.SOAPUI_SSL_KEYSTORE_PASSWORD,
                    settings.getString(SSLSettings.KEYSTORE_PASSWORD, ""));

            char[] pwd = pass.toCharArray();

            if (keyStoreUrl.trim().length() > 0) {
                File f = new File(keyStoreUrl);
                if (f.exists()) {
                    log.info("Initializing KeyStore");

                    try {
                        KeyMaterial km = new KeyMaterial(f, pwd);
                        keyStore = km.getKeyStore();
                    } catch (Exception e) {
                        SoapUI.logError(e);
                    }
                }
            }

            return new SoapUISSLSocketFactory(keyStore, pass);
        }
    }

    public static SoapUIHttpClient getHttpClient() {
        return helper.getHttpClient();
    }

    public static void setProxySelector(ProxySelector proxySelector) {
        getHttpClient().setRoutePlanner(new OverridableProxySelectorRoutePlanner(helper.getRegistry(), proxySelector));
    }

    public static HttpResponse execute(ExtendedHttpMethod method, HttpContext httpContext)
            throws ClientProtocolException, IOException {
        return helper.execute(method, httpContext);
    }

    public static HttpResponse execute(ExtendedHttpMethod method) throws ClientProtocolException, IOException {
        return helper.execute(method);
    }

    public static void applyHttpSettings(HttpRequest httpMethod, Settings settings) {
        // user agent?
        String userAgent = settings.getString(HttpSettings.USER_AGENT, null);
        if (userAgent != null && userAgent.length() > 0) {
            httpMethod.setHeader("User-Agent", userAgent);
        }

        // timeout?
        long timeout = settings.getLong(HttpSettings.SOCKET_TIMEOUT, HttpSettings.DEFAULT_SOCKET_TIMEOUT);
        httpMethod.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, (int) timeout);
    }

    public static String getResponseCompressionType(HttpResponse httpResponse) {
        Header contentType = null;
        if (httpResponse.getEntity() != null) {
            contentType = httpResponse.getEntity().getContentType();
        }

        Header contentEncoding = null;
        if (httpResponse.getEntity() != null) {
            contentEncoding = httpResponse.getEntity().getContentEncoding();
        }

        return getCompressionType(contentType == null ? null : contentType.getValue(), contentEncoding == null ? null
                : contentEncoding.getValue());
    }

    public static String getCompressionType(String contentType, String contentEncoding) {
        String compressionAlg = contentType == null ? null : CompressionSupport.getAvailableAlgorithm(contentType);
        if (compressionAlg != null) {
            return compressionAlg;
        }

        if (contentEncoding == null) {
            return null;
        } else {
            return CompressionSupport.getAvailableAlgorithm(contentEncoding);
        }
    }

    public static void addSSLListener(Settings settings) {
        settings.addSettingsListener(helper.new SSLSettingsListener());
    }

    public static BasicHttpContext createEmptyContext() {
        BasicHttpContext httpContext = new BasicHttpContext();

        // always use local cookie store so we don't share cookies with other threads/executions/requests
        CookieStore cookieStore = new BasicCookieStore();
        httpContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);

        return httpContext;
    }

}
